Source code for hysop.operator.memory_reordering

# Copyright (c) HySoP 2011-2024
#
# This file is part of HySoP software.
# See "https://particle_methods.gricad-pages.univ-grenoble-alpes.fr/hysop-doc/"
# for further info.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


"""
@file memory_reordering.py
Memory ordering related operators.
"""

from hysop.constants import Implementation, Backend, MemoryOrdering
from hysop.tools.htypes import check_instance, to_tuple, first_not_None
from hysop.tools.decorators import debug
from hysop.fields.continuous_field import Field, ScalarField

from hysop.topology.cartesian_descriptor import CartesianTopologyDescriptors
from hysop.core.graph.node_generator import ComputationalGraphNodeGenerator
from hysop.core.graph.computational_operator import ComputationalGraphNode


[docs] class MemoryReorderingNotImplementedError(NotImplementedError): """ Error raised for unimplemented memory reordering operator configurations. """ pass
[docs] class MemoryReordering(ComputationalGraphNodeGenerator): """ Operator generator for inplace and out of place field memory reordering. Available implementations are: *python (numpy based memory reordering, n-dimensional) This is currently used to convert C_CONTIGUOUS fields to F_CONTIGUOUS fields. Implementations handle only one field at a time but this graph node generator generates an operator per supplied field, possibly on different implementation backends. See hysop.operator.base.reorder.MemoryReorderingBase for operator backend implementation interface. """
[docs] @classmethod def implementations(cls): from hysop.backend.host.python.operator.memory_reordering import ( PythonMemoryReordering, ) from hysop.backend.device.opencl.operator.memory_reordering import ( OpenClMemoryReordering, ) _implementations = { Implementation.PYTHON: PythonMemoryReordering, Implementation.OPENCL: OpenClMemoryReordering, } return _implementations
[docs] @classmethod def default_implementation(cls): msg = "MemoryReordering has no default implementation, " msg += "implementation should match the discrete field topology backend." raise RuntimeError(msg)
@debug def __new__( cls, fields, variables, target_memory_order, output_fields=None, implementation=None, name=None, base_kwds=None, **kwds, ): base_kwds = first_not_None(base_kwds, {}) return super().__new__( cls, name=name, candidate_input_tensors=None, candidate_output_tensors=None, **base_kwds, ) @debug def __init__( self, fields, variables, target_memory_order, output_fields=None, implementation=None, name=None, base_kwds=None, **kwds, ): """ Initialize a MemoryReordering operator generator operating on CartesianTopology topologies. MemoryReordering is deduced from topology requirements. Parameters ---------- fields: Field, list or tuple of Fields Input continuous fields to be memory reordered, at least 2D. All fields should have the same dimension. output_fields: Field, list or tuple of Fields, optional Output continuous fields where the results are stored. Reordered shapes should match the input shapes. By default output_fields are the same as input_fields resulting in inplace memory reordering. Input and output are matched by order int list/tuple. variables: dict Dictionary of fields as keys and CartesianTopologyDescriptors as values. target_memory_order: MemoryOrdering Target memory order to achieve. implementation: Implementation, optional, defaults to None target implementation, should be contained in available_implementations(). If implementation is set and topology does not match backend, RuntimeError will be raised on _generate. If None, implementation will be set according to topologies backend, different implementations may be choosen for different Fields if defined on different backends. name: string prefix for generated operator names base_kwds: dict, optional, defaults to None Base class keywords arguments. If None, an empty dict will be passed. kwds: Keywords arguments that will be passed towards implementation memory reordering operator __init__. Notes ----- Out of place reordering will always be faster to process. In place reordering requires an extra buffer and an extra copy. * About dimensions: - No limit. A MemoryReordering operator implementation should support the MemoryReorderingBase interface (see hysop.operator.base.memory_ordering.MemoryOrdering). This ComputationalGraphNodeFrontend will generate a operator for each input and output ScalarField pair. All implementations should raise MemoryReorderingNotImplementedError if the user supplied parameters leads to unimplemented or unsupported memory reordering features. """ input_fields = to_tuple(fields) assert None not in input_fields if output_fields is not None: output_fields = to_tuple(output_fields) output_fields = tuple( ofield if (ofield is not None) else ifield for (ifield, ofield) in zip(input_fields, output_fields) ) else: output_fields = tuple(ifield for ifield in input_fields) check_instance(input_fields, tuple, values=Field) check_instance(output_fields, tuple, values=Field, size=len(input_fields)) check_instance(target_memory_order, MemoryOrdering) assert target_memory_order in ( MemoryOrdering.C_CONTIGUOUS, MemoryOrdering.F_CONTIGUOUS, ) candidate_input_tensors = tuple(filter(lambda x: x.is_tensor, input_fields)) candidate_output_tensors = tuple(filter(lambda x: x.is_tensor, output_fields)) base_kwds = first_not_None(base_kwds, {}) if not "mpi_params" in base_kwds: mpi_params = next(iter(variables.values())).mpi_params assert all([_.mpi_params == mpi_params for _ in variables.values()]) kwds.update({"mpi_params": mpi_params}) super().__init__( name=name, candidate_input_tensors=candidate_input_tensors, candidate_output_tensors=candidate_output_tensors, **base_kwds, ) # expand tensors ifields, ofields = (), () for ifield, ofield in zip(input_fields, output_fields): msg = "Input and output field shape mismatch, got field {} of shape {} " msg += "and field {} of shape {}." if ifield.is_tensor ^ ofield.is_tensor: if ifield.is_tensor: msg = msg.format( ifield.short_description(), ifield.shape, ofield.short_description(), "(1,)", ) else: msg = msg.format( ifield.short_description(), "(1,)", ofield.short_description(), ofield.shape, ) raise RuntimeError(msg) if ifield.is_tensor and ofield.is_tensor and (ifield.shape != ofield.shape): msg = msg.format( ifield.short_description(), ifield.shape, ofield.short_description(), ofield.shape, ) raise RuntimeError(msg) ifields += ifield.fields ofields += ofield.fields input_fields = ifields output_fields = ofields check_instance(input_fields, tuple, values=ScalarField) check_instance(output_fields, tuple, values=ScalarField, size=len(input_fields)) check_instance(variables, dict, keys=Field, values=CartesianTopologyDescriptors) check_instance(base_kwds, dict, keys=str) check_instance(name, str, allow_none=True) fields = set(input_fields).union(output_fields) vfields = {f for tfield in variables.keys() for f in tfield.fields} if not fields: raise ValueError("fields are empty.") if not vfields: raise ValueError("variables are empty.") if fields != vfields: if fields.difference(vfields): missing = tuple(field.name for field in fields - vfields) msg = "Missing fields in variables parameter: {}" msg = msg.format(", ".join(missing)) else: missing = tuple(field.name for field in vfields - fields) msg = "Too many fields present in variables keys: {}" msg = msg.format(", ".join(missing)) raise ValueError(msg) dim = input_fields[0].domain.dim if dim < 2: msg = "input_field dimension should be at least 2 but {} is a {}D field." msg = msg.format(input_fields[0].name, dim) raise ValueError(msg) for input_field, output_field in zip(input_fields, output_fields): in_topo_descriptor = ComputationalGraphNode.get_topo_descriptor( variables, input_field ) out_topo_descriptor = ComputationalGraphNode.get_topo_descriptor( variables, output_field ) if input_field.domain != output_field.domain: msg = "input_field {} and output_field {} do not share the same domain." msg.format(input_field.name, output_field.name) raise ValueError(msg) idim = input_field.domain.dim if idim != dim: msg = "input_field {} is of dimension {} and does not match first " msg += "input_field {} dimension {}." msg.format(input_field, idim, input_fields[0].name, dim) raise ValueError(msg) self.input_fields = input_fields self.output_fields = output_fields self.variables = variables self.implementation = implementation self.target_memory_order = target_memory_order self.kwds = kwds def _get_op_and_check_implementation(self, src_topo, dst_topo): backend = getattr(src_topo, "backend", None) or getattr( dst_topo, "backend", None ) implementation = self.implementation if backend is None: if implementation is None: msg = ( "Source and destination topology descriptors do not expose backend " ) msg += ( "attribute and self.implementation was not set, cannot determine " ) msg += "the target memory reordering operator." raise ValueError(msg) if implementation not in self.implementations(): msg = f"Unknown memory reordering implementation {implementation}." raise ValueError(msg) op_cls = self.implementations()[implementation] else: def check_impl(impl, allowed): if impl not in allowed: msg = "Specified memory reordering implementation {} but this is not a valid " msg += "implementation for fields defined on backend of kind {}." msg = msg.format(impl, backend.kind) raise ValueError(msg) if backend.kind == Backend.HOST: implementation = first_not_None( self.implementation, Implementation.PYTHON ) check_impl(implementation, (Implementation.PYTHON,)) op_cls = self.implementations()[implementation] elif backend.kind == Backend.OPENCL: implementation = first_not_None( self.implementation, Implementation.OPENCL ) check_impl(implementation, (Implementation.OPENCL,)) op_cls = self.implementations()[implementation] else: msg = f"Unsupported memory reordering backend {backend.kind}." raise ValueError(msg) from hysop.operator.base.memory_reordering import MemoryReorderingBase if not issubclass(op_cls, MemoryReorderingBase): msg = ( "Class {} does not inherit from the memory ordering operator interface " ) msg += "({}). This is an implementation error." msg = msg.format(op_cls, MemoryReorderingBase) raise TypeError(msg) if not issubclass(op_cls, ComputationalGraphNode): msg = ( "Class {} does not inherit from the computational graph node interface" ) msg += "({}). This is an implementation error." msg = msg.format(op_cls, ComputationalGraphNode) raise TypeError(msg) return op_cls @debug def _generate(self): nodes = [] for ifield, ofield in zip(self.input_fields, self.output_fields): src_topo = ComputationalGraphNode.get_topo_descriptor( self.variables, ifield ) dst_topo = ComputationalGraphNode.get_topo_descriptor( self.variables, ofield ) MemoryReorderingOp = self._get_op_and_check_implementation( src_topo, dst_topo ) kwds = self.kwds.copy() variables = {ifield: src_topo} variables[ofield] = dst_topo # instantiate operator node = MemoryReorderingOp( input_field=ifield, output_field=ofield, variables=variables, target_memory_order=self.target_memory_order, **kwds, ) nodes.append(node) if not nodes: msg = "No memory reordering needed for given input and output fields." raise RuntimeError(msg) return nodes